/*
    This file is part of XCOM Mod Manager.

    XCOM Mod Manager is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    XCOM Mod Manager is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with XCOM Mod Manager.  If not, see <http://www.gnu.org/licenses/>.
*/
/* 
 * @author Anthony Surma
 */

import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import javax.xml.bind.DatatypeConverter;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;


final public class ModInstaller
{    
    final static String MAINCONFIG     = "config\\config.xml"; 
    final static String XCOMFOLDERTAG  = "XCOM";    
    final static String COOKED         = "XComGame\\CookedPCConsole";
    final static String UPKFILENAMETAG = "UPKFilename";
    final static String DIFFDATATAG    = "DiffData";
    final static String SEARCHHASHTAG  = "SearchHash";
    final static String DATASUMTAG     = "DataSum";
    final static String UNPACKEDHEADER = "C1832A9E4D";
    final static int    NUMTHREADS     = Runtime.getRuntime().availableProcessors();
        
    final class UPKData
    {
        final File      upkFile;
        final int       dataSum;
        final byte[]    searchHash;
        final int       searchHashLength;
        final ModDiff[] diffs;
        boolean         isInstalled = false;
        
        UPKData(final File upkFile, final String dataSum, final String searchHash, final String diffs)
        {
            this.upkFile          = upkFile;
            this.dataSum          = Integer.parseInt(dataSum);
            String split[]        = searchHash.split("#");
            this.searchHashLength = Integer.parseInt(split[0]);
            this.searchHash       = hexStringToBytes(split[1]);
            this.diffs            = parseDiffDataString(diffs);
        }   
        
        ModDiff[] parseDiffDataString(final String diffDataString)
        {
            final String    tokenized[] = diffDataString.split("#");
            final int       length      = tokenized.length;
            final ModDiff[] result      = new ModDiff[length];

            for (int i = 0; i < length; ++i)
            {
                String[] tokens = tokenized[i].split("/");
                result[i] = new ModDiff(Integer.parseInt(tokens[0]), tokens[1]);            
            }

            return result;        
        }                               
    }
    
    final class ModDiff
    {
        final int  offset;
        final byte data;
        
        ModDiff(final int offset, final String data)
        {
            this.offset = offset;
            this.data   = DatatypeConverter.parseHexBinary(data)[0];
        }                                                         
    }
    
    public static void main(final String[] args) throws SAXException, IOException, NoSuchAlgorithmException, ParserConfigurationException, TransformerConfigurationException, TransformerException, FileNotFoundException, InterruptedException
    {         
        final File         modFile;
        final File         mainConfigFile;       
        final String[]     list;
        final String[]     upkFilenames;
        final String       xcomFolderPath;     
        final File[]       upkFiles;
        final String[]     searchHashes;       
        final String[]     diffDataStrings;        
        final String[]     dataSums;
        final UPKData[]    upkData;
        final ModLogFile   mlf;
        final ModInstaller mi = new ModInstaller();       
        
        if (args.length != 1) exitError(1);
                         
        modFile = new File(fixModFilePath(args[0]));        
        if (!modFile.exists()) exitError(2);
        
        mainConfigFile = new File(MAINCONFIG);        
        if (!mainConfigFile.exists()) exitError(3);
        
        list = searchXML(XCOMFOLDERTAG, MAINCONFIG);              
        if (list.length != 1) exitError(4);
        xcomFolderPath = list[0] + "\\";
                                                               
        upkFilenames = searchXML(UPKFILENAMETAG, modFile.getPath());        
        upkFiles = new File[upkFilenames.length];
        
        for (int i = 0; i < upkFilenames.length; ++i)
        {          
            File f = new File(xcomFolderPath + COOKED + "\\" + upkFilenames[i]);
            if (!f.exists()) exitError(5);
            if (!isUnpacked(f)) exitError(6);
            upkFiles[i] = f;            
        }
                       
        diffDataStrings = searchXML(DIFFDATATAG, modFile.getPath());
        if (diffDataStrings.length != upkFiles.length) exitError(7);      
        
        searchHashes = searchXML(SEARCHHASHTAG, modFile.getPath());
        if (searchHashes.length != upkFiles.length) exitError(8);       
        
        dataSums = searchXML(DATASUMTAG, modFile.getPath());
        if (dataSums.length != upkFiles.length) exitError(9);    
        
        upkData = mi.getUPKData(upkFiles, dataSums, searchHashes, diffDataStrings);                        
        mi.doDiffs(upkData);                
        
        mlf = new ModLogFile();        
        mlf.writeLog(modFile, upkData);                
    }
            
    void doDiffs(final UPKData[] upkData) throws FileNotFoundException, IOException, NoSuchAlgorithmException, InterruptedException
    {                    
        for (int i = 0; i < upkData.length; ++i)
        {                                                                        
            final RandomAccessFile raf = new RandomAccessFile(upkData[i].upkFile, "rw");            
            final int hashDataLength   = upkData[i].searchHashLength;
            final byte[] buffer        = new byte[(int)upkData[i].upkFile.length() + hashDataLength];                                                  
            final Thread[] threads     = new Thread[NUMTHREADS];
            final int length           = buffer.length / NUMTHREADS;                                                                        
            final Worker[] workers     = new Worker[NUMTHREADS];
            final CountDownLatch mainCountDownLatch   = new CountDownLatch(1);
            final CountDownLatch workerCountDownLatch = new CountDownLatch(NUMTHREADS);
            
            raf.read(buffer);
            
            for (int j = 0; j < NUMTHREADS; ++j)
            {        
                int start = j * length;
                int end   = start + (length - 1); 
                workers[j] = new Worker(upkData[i], buffer, start , end, 
                        MessageDigest.getInstance("SHA"), mainCountDownLatch, 
                        workerCountDownLatch, threads);
                threads[j] = new Thread(workers[j]);
            }
            
            for (Thread t : threads) t.start();
                                    
            mainCountDownLatch.await();
            
            for (Worker w : workers)
            {
                if (w.resultBytes != null)
                {
                    raf.seek(w.resultInt);
                    raf.write(w.resultBytes);
                    break;
                }
            }
            
            raf.close();
        }              
    }
    
    UPKData[] getUPKData(final File[] upkFiles, final String[] dataSums, final String[] searchHashes, final String[] diffDataStrings)
    {        
        final int length = upkFiles.length;
        final UPKData[] result = new UPKData[length];
        
        for (int i = 0; i < length; ++i) result[i] = new UPKData(upkFiles[i], dataSums[i], searchHashes[i], diffDataStrings[i]);
                                    
        return result;        
    }
    
    static String fixModFilePath(final String arg)
    {
        final StringBuilder fixed = new StringBuilder();
        final String[] tokens = arg.split("\\\\");
                
        for (final String s : tokens) fixed.append(s).append("\\\\");
                
        fixed.setLength(fixed.length() - 2);
        
        return fixed.toString();
    }
    
    static String[] searchXML(final String searchString, final String filename) throws SAXException, IOException
    {       
        final XMLReader reader = XMLReaderFactory.createXMLReader();        
        final DocHandler handler = new DocHandler(searchString);
                
        reader.setContentHandler(handler);
        reader.parse(filename);
        
        return handler.getResults();
    }
    
    static void exitError(final int errorCode)
    {
        System.out.println("ErrorCode " + errorCode);
        System.exit(errorCode);
    }
    
    static boolean isUnpacked(final File f) throws FileNotFoundException, IOException
    {
        final FileInputStream fis = new FileInputStream(f);
        final byte[] bHeader      = hexStringToBytes(UNPACKEDHEADER);
        final byte[] bArray       = new byte[bHeader.length];
        
        fis.read(bArray);
        
        for (int i = bArray.length - 1; i >= 0; --i) if (bArray[i] != bHeader[i]) return false;
                            
        return true;        
    }
    
    static byte[] hexStringToBytes(final String s)
    {
        return DatatypeConverter.parseHexBinary(s);
    }
    
    static String bytesToHexString(final byte[] b)
    {
        final StringBuilder result = new StringBuilder(20);
                
        for (int i = 0; i < b.length; ++i) 
        {                             
            String hexString = Integer.toHexString((int)b[i] & 0xff);           
            if (hexString.length() == 1) hexString = "0" + hexString;  
            result.append(hexString);
        }           
        
        return result.toString();
    }
}